use std::str::FromStr;

use crate::{constants::SHEET_NAME, util::case_fold};

use super::{Grid, Sheet, SheetId};
use anyhow::{Context, Result, anyhow};
use indexmap::IndexMap;
use lexicon_fractional_index::key_between;

impl Grid {
    /// Returns a list of all sheets by ID.
    pub fn sheets(&self) -> &IndexMap<SheetId, Sheet> {
        &self.sheets
    }

    /// Returns the ID of the first sheet.
    ///
    /// # Panics
    ///
    /// This method panics if the grid contains no sheets.
    pub fn first_sheet_id(&self) -> SheetId {
        if self.sheets.is_empty() {
            unreachable!("grid should always have at least one sheet");
        }
        self.sheets[0].id
    }

    /// Returns the first sheet.
    ///
    /// # Panics
    ///
    /// This method panics if the grid contains no sheets.
    pub fn first_sheet(&self) -> &Sheet {
        let id = self.first_sheet_id();
        self.try_sheet(id)
            .expect("there should always be a first sheet in the grid")
    }

    /// Returns a mutable reference to the first sheet.
    ///
    /// # Panics
    ///
    /// This method panics if the grid contains no sheets.
    pub fn first_sheet_mut(&mut self) -> &mut Sheet {
        let id = self.first_sheet_id();
        self.try_sheet_mut(id)
            .expect("there should always be a first sheet in the grid")
    }

    /// Returns a list of all sheet IDs in order.
    pub fn sheet_ids(&self) -> Vec<SheetId> {
        self.sheets.keys().copied().collect()
    }

    /// Returns the sheet with the given name.
    ///
    /// This runs in O(n) time.
    ///
    /// `name` is automatically case-folded.
    pub fn try_sheet_from_name(&self, name: &str) -> Option<&Sheet> {
        let name = case_fold(name.trim());
        self.sheets
            .values()
            .find(|sheet| case_fold(&sheet.name) == name)
    }

    /// Returns a mutable reference to the sheet with the given name.
    ///
    /// This runs in O(n) time.
    ///
    /// `name` is automatically case-folded.
    pub fn try_sheet_mut_from_name(&mut self, name: &str) -> Option<&mut Sheet> {
        let name = case_fold(name.trim());
        self.sheets
            .values_mut()
            .find(|sheet| case_fold(&sheet.name) == name)
    }

    /// Parses `id` to a `SheetId` and returns the sheet with that ID.
    pub fn try_sheet_from_string_id(&self, id: &str) -> Option<&Sheet> {
        SheetId::from_str(id).map_or(None, |sheet_id| self.try_sheet(sheet_id))
    }

    /// Parses `id` to a `SheetId` and returns a mutable reference to the sheet
    /// with that ID.
    pub fn try_sheet_mut_from_string_id(&mut self, id: &str) -> Option<&mut Sheet> {
        SheetId::from_str(id).map_or(None, |sheet_id| self.try_sheet_mut(sheet_id))
    }

    /// Sorts sheets according to their `order` fields.
    pub fn sort_sheets(&mut self) {
        self.sheets
            .sort_by(|_id1, sheet1, _id2, sheet2| sheet1.order.cmp(&sheet2.order));
    }

    /// Returns the `order` field to use for a new sheet that is placed at the
    /// end of the sheet list.
    pub fn end_order(&self) -> String {
        let last_order = self.sheets.last().map(|(_id, last)| last.order.clone());
        key_between(last_order.as_deref(), None).unwrap()
    }

    /// Returns the `order` field of the sheet immediately before `sheet_id`.
    pub fn previous_sheet_order(&self, sheet_id: SheetId) -> Option<&str> {
        let i = self.sheets.get_index_of(&sheet_id)?;
        let (_, sheet) = self.sheets.get_index(i.checked_sub(1)?)?;
        Some(&sheet.order)
    }

    /// Returns the sheet immediately after `sheet_id`.
    pub fn next_sheet(&self, sheet_id: SheetId) -> Option<&Sheet> {
        let i = self.sheets.get_index_of(&sheet_id)?;
        let (_, sheet) = self.sheets.get_index(i + 1)?;
        Some(sheet)
    }

    /// Returns a unique sheet name for a given name.
    pub fn unique_sheet_name(&self, name: &str) -> String {
        let mut unique_name = name.to_owned();
        let mut index = 1;
        loop {
            let folded_new_name = case_fold(&unique_name);
            if self
                .sheets
                .values()
                .any(|old_sheet| case_fold(&old_sheet.name) == folded_new_name)
            {
                unique_name = format!("{}({})", unique_name, index);
                index += 1;
            } else {
                break;
            }
        }
        unique_name
    }

    /// Adds a sheet to the grid. Returns an error if the sheet name is already
    /// in use.
    ///
    /// If `sheet` is `None`, a new sheet is created with a random ID and
    /// autogenerated name.
    ///
    /// Adds a suffix if the sheet already exists.
    pub fn add_sheet(&mut self, sheet: Option<Sheet>) -> SheetId {
        // for new sheets, order is after the last one
        let mut new_sheet = sheet.unwrap_or_else(|| {
            Sheet::new(
                SheetId::new(),
                format!("{}{}", SHEET_NAME.to_owned(), self.sheets.len() + 1),
                self.end_order(),
            )
        });

        let new_id = new_sheet.id;

        // Ensure the name is unique
        new_sheet.name = self.unique_sheet_name(&new_sheet.name);

        self.sheets.insert(new_id, new_sheet);
        self.sort_sheets();
        new_id
    }

    /// Removes the sheet with ID `sheet_id`.
    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Option<Sheet> {
        let i = self.sheets.get_index_of(&sheet_id);
        match i {
            Some(i) => Some(self.sheets.shift_remove_index(i)?.1),
            None => None,
        }
    }

    /// Sets the `order` for the sheet with ID `target` reorders sheets
    /// appropriately.
    pub fn move_sheet(&mut self, target: SheetId, order: String) {
        if let Some(target) = self.try_sheet_mut(target) {
            target.order = order;
            self.sort_sheets();
        }
    }

    /// Returns the index of a sheet.
    pub fn sheet_id_to_index(&self, id: SheetId) -> Option<usize> {
        self.sheets.get_index_of(&id)
    }

    /// Returns the sheet with the given ID.
    pub fn try_sheet(&self, sheet_id: SheetId) -> Option<&Sheet> {
        self.sheets.get(&sheet_id)
    }

    /// Returns the sheet with the given ID, or a user-friendly error if it does
    /// not exist.
    pub fn try_sheet_result(&self, sheet_id: SheetId) -> Result<&Sheet> {
        self.try_sheet(sheet_id)
            .ok_or_else(|| anyhow!("Sheet not found: {sheet_id:?}"))
    }

    /// Returns a mutable reference to the sheet with the given ID.
    pub fn try_sheet_mut(&mut self, sheet_id: SheetId) -> Option<&mut Sheet> {
        self.sheets.get_mut(&sheet_id)
    }

    /// Returns a mutable reference to the sheet with the given ID,
    /// or a user-friendly error if it does not exist.
    pub fn try_sheet_mut_result(&mut self, sheet_id: SheetId) -> Result<&mut Sheet> {
        self.try_sheet_mut(sheet_id)
            .ok_or_else(|| anyhow!("Sheet not found: {:?}", sheet_id))
    }

    /// Updates a sheet's name and returns the old name.
    pub fn update_sheet_name(&mut self, sheet_id: SheetId, new_name: &str) -> Result<String> {
        let sheet = self.try_sheet_mut(sheet_id).context("missing sheet")?;
        if sheet.name == new_name {
            return Ok(sheet.name.clone());
        }

        let old_name = std::mem::replace(&mut sheet.name, new_name.to_owned());

        for sheet in self.sheets.values_mut() {
            sheet
                .data_tables
                .replace_sheet_name_in_code_cells(sheet.id, &old_name, new_name);
        }

        Ok(old_name)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_try_sheet_from_id() {
        let grid = Grid::new();
        let id = grid.first_sheet_id();

        assert!(grid.try_sheet(id).is_some());
        assert!(grid.try_sheet(SheetId::new()).is_none());
    }

    #[test]
    fn test_try_sheet_mut_from_id() {
        let mut grid = Grid::new();
        let id = grid.first_sheet_id();

        assert!(grid.try_sheet_mut(id).is_some());
        assert!(grid.try_sheet_mut(SheetId::new()).is_none());
    }

    /// creates three sheets with name 1, 2, and 3
    fn create_three_sheets() -> Grid {
        let mut grid = Grid::new();
        grid.sheets[0].name = String::from('0');
        grid.add_sheet(None);
        grid.sheets[1].name = String::from('1');
        grid.add_sheet(None);
        grid.sheets[2].name = String::from('2');
        grid
    }

    #[test]
    fn test_order_add_sheet() {
        let grid = create_three_sheets();
        let sheet_0 = &grid.sheets[0];
        let sheet_1 = &grid.sheets[1];
        let sheet_2 = &grid.sheets[2];
        assert!(sheet_0.order < sheet_1.order);
        assert!(sheet_1.order < sheet_2.order);
    }

    #[test]
    fn test_order_move_sheet() {
        // starting as name = 0, 1, 2
        let mut grid = create_three_sheets();

        // moved to name = 1, 0, 2
        grid.move_sheet(
            grid.sheets[0].id,
            key_between(Some(&grid.sheets[1].order), Some(&grid.sheets[2].order)).unwrap(),
        );
        assert_eq!(grid.sheets[0].name, String::from('1'));
        assert_eq!(grid.sheets[1].name, String::from('0'));
        assert_eq!(grid.sheets[2].name, String::from('2'));

        // moved to name = 1, 2, 0
        grid.move_sheet(
            grid.sheets[1].id,
            key_between(Some(&grid.sheets[2].order), None).unwrap(),
        );
        assert_eq!(grid.sheets[0].name, String::from('1'));
        assert_eq!(grid.sheets[1].name, String::from('2'));
        assert_eq!(grid.sheets[2].name, String::from('0'));

        // moved back to name = 0, 1, 2
        grid.move_sheet(
            grid.sheets[2].id,
            key_between(None, Some(&grid.sheets[0].order)).unwrap(),
        );
        assert_eq!(grid.sheets[0].name, String::from('0'));
        assert_eq!(grid.sheets[1].name, String::from('1'));
        assert_eq!(grid.sheets[2].name, String::from('2'));
    }

    #[test]
    fn test_first_sheet() {
        let grid = create_three_sheets();
        assert_eq!(grid.first_sheet().name, String::from('0'));
    }

    #[test]
    fn test_first_sheet_id() {
        let grid = create_three_sheets();
        assert_eq!(grid.first_sheet_id(), grid.sheets[0].id);
    }

    #[test]
    fn test_previous_sheet_order() {
        let grid = create_three_sheets();
        assert_eq!(grid.previous_sheet_order(grid.sheets[0].id), None);
        assert_eq!(
            grid.previous_sheet_order(grid.sheets[1].id),
            Some(grid.sheets[0].order.as_str())
        );
        assert_eq!(
            grid.previous_sheet_order(grid.sheets[2].id),
            Some(grid.sheets[1].order.as_str())
        );
    }

    #[test]
    fn test_next_sheet() {
        let grid = create_three_sheets();
        assert_eq!(grid.next_sheet(grid.sheets[0].id), Some(&grid.sheets[1]));
        assert_eq!(grid.next_sheet(grid.sheets[1].id), Some(&grid.sheets[2]));
        assert_eq!(grid.next_sheet(grid.sheets[2].id), None);
    }

    #[test]
    fn test_sort_sheets() {
        let mut grid = create_three_sheets();
        grid.sheets[0].order = String::from("a2");
        grid.sheets[1].order = String::from("a1");
        grid.sheets[2].order = String::from("a3");
        grid.sort_sheets();
        assert_eq!(grid.sheets[0].name, String::from('1'));
        assert_eq!(grid.sheets[1].name, String::from('0'));
        assert_eq!(grid.sheets[2].name, String::from('2'));
    }

    #[test]
    fn test_move_sheet() {
        let mut grid = create_three_sheets();
        grid.move_sheet(
            grid.sheets[0].id,
            key_between(Some(&grid.sheets[1].order), Some(&grid.sheets[2].order)).unwrap(),
        );
        assert_eq!(grid.sheets[0].name, String::from('1'));
        assert_eq!(grid.sheets[1].name, String::from('0'));
        assert_eq!(grid.sheets[2].name, String::from('2'));
    }

    #[test]
    fn add_sheet_adds_suffix_if_name_already_in_use() {
        let mut grid = Grid::new();
        grid.add_sheet(Some(Sheet::new(
            SheetId::new(),
            format!("{}1", SHEET_NAME.to_owned()),
            "a1".to_string(),
        )));
        grid.add_sheet(Some(Sheet::new(
            SheetId::new(),
            format!("{}1", SHEET_NAME.to_owned()),
            "a1".to_string(),
        )));
        assert_eq!(grid.sheets[0].name, format!("{}1", SHEET_NAME.to_owned()));
        assert_eq!(
            grid.sheets[1].name,
            format!("{}1(1)", SHEET_NAME.to_owned())
        );
    }
}
